/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package backend.core.settings;



import backend.core.common.SetOnce;
import backend.core.common.hash.MessageDigests;
import backend.toolkit.KeyStoreUtils;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.*;
import java.util.regex.Pattern;


public class KeyStoreWrapper implements SecureSettings {

    /**
     * A regex for the valid characters that a setting name in the keystore may use.
     */
    private static final Pattern ALLOWED_SETTING_NAME = Pattern.compile("[A-Za-z0-9_\\-.]+");

    /**
     * Ensure the given setting name is allowed.
     *
     * @throws IllegalArgumentException if the setting name is not valid
     */
    public static void validateSettingName(String setting) {
        if (ALLOWED_SETTING_NAME.matcher(setting).matches() == false) {
            throw new IllegalArgumentException(
                    "Setting name [" + setting + "] does not match the allowed setting name pattern [" + ALLOWED_SETTING_NAME.pattern() + "]"
            );
        }
    }

    /** The name of the keystore file to read and write. */
    public static final String KEYSTORE_FILENAME = "mysql.keystore";

    /** The oldest metadata format version that can be read. */
    private static final int MIN_FORMAT_VERSION = 3;
    /** Legacy versions of the metadata written before the keystore data. */
    public static final int V2_VERSION = 2;
    public static final int V3_VERSION = 3;
    public static final int V4_VERSION = 4;
    /** The version where lucene directory API changed from BE to LE. */
    public static final int LE_VERSION = 5;
    public static final int CURRENT_VERSION = LE_VERSION;


    /** The algorithm used to derive the cipher key from a password. */
    private static final String KDF_ALGO = "PBKDF2WithHmacSHA512";

    /** The number of iterations to derive the cipher key. */
    private static final int KDF_ITERS = 10000;

    /**
     * The number of bits for the cipher key.
     *
     * Note: The Oracle JDK 8 ships with a limited JCE policy that restricts key length for AES to 128 bits.
     * This can be increased to 256 bits once minimum java 9 is the minimum java version.
     * See http://www.oracle.com/technetwork/java/javase/terms/readme/jdk9-readme-3852447.html#jce
     * */
    private static final int CIPHER_KEY_BITS = 128;

    /** The number of bits for the GCM tag. */
    private static final int GCM_TAG_BITS = 128;

    /** The cipher used to encrypt the keystore data. */
    private static final String CIPHER_ALGO = "AES";

    /** The mode used with the cipher algorithm. */
    private static final String CIPHER_MODE = "GCM";

    /** The padding used with the cipher algorithm. */
    private static final String CIPHER_PADDING = "NoPadding";

    // format version changelog:
    // 1: initial version, ES 5.3
    // 2: file setting, ES 5.4
    // 3: FIPS compliant algos, ES 6.3
    // 4: remove distinction between string/files, ES 6.8/7.1
    // 5: Lucene directory API changed to LE, ES 8.0

    /** The metadata format version used to read the current keystore wrapper. */
    private final int formatVersion;

    /** True iff the keystore has a password needed to read. */
    private final boolean hasPassword;

    /** The raw bytes of the encrypted keystore. */
    private final byte[] dataBytes;

    private final SetOnce<Map<String, Entry>> entries = new SetOnce<>();
    private volatile boolean closed;


    private KeyStoreWrapper(int formatVersion, boolean hasPassword, byte[] dataBytes) {
        this.formatVersion = formatVersion;
        this.hasPassword = hasPassword;
        this.dataBytes = dataBytes;
    }

    /**
     * Get the metadata format version for the keystore
     **/
    public int getFormatVersion() {
        return formatVersion;
    }

    /** Returns a path representing the ES keystore in the given config dir. */
    public static Path keystorePath(Path configDir) {
        return configDir.resolve(KEYSTORE_FILENAME);
    }

    /** Constructs a new keystore with the given password. */
    public static KeyStoreWrapper create() {
        KeyStoreWrapper wrapper = new KeyStoreWrapper(CURRENT_VERSION, false, null);
        wrapper.entries.set(new HashMap<>());
        return wrapper;
    }

    @Override
    public boolean isLoaded() {
        return false;
    }

    @Override
    public Set<String> getSettingNames() {
        return null;
    }

    @Override
    public SecureString getString(String setting) {
        return null;
    }

    @Override
    public InputStream getFile(String setting) throws GeneralSecurityException {
        return null;
    }

    @Override
    public byte[] getSHA256Digest(String setting) throws GeneralSecurityException {
        return new byte[0];
    }

    @Override
    public void close() throws IOException {

    }


    // TODO: this doesn't need to be a supplier anymore
    public static KeyStoreWrapper bootstrap(Path configDir, SecureString keystorePassword) throws Exception {
        KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);

        return keystore;
    }

    private boolean hasPassword() {
        return hasPassword;
    }


    /**
     * Loads information about the Elasticsearch keystore from the provided config directory.
     *
     * {@link #decrypt(char[])} must be called before reading or writing any entries.
     * Returns {@code null} if no keystore exists.
     */
    public static KeyStoreWrapper load(Path configDir) throws IOException {
        Path keystoreFile = keystorePath(configDir);
        if (Files.exists(keystoreFile) == false) {
            return null;
        }

        byte[] dataBytes= new byte[20];
        return new KeyStoreWrapper(1, false, dataBytes);



    }



    private static class Entry {
        final byte[] bytes;
        final byte[] sha256Digest;

        Entry(byte[] bytes) {
            this.bytes = bytes;
            this.sha256Digest = MessageDigests.sha256().digest(bytes);
        }

    }
}
